﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Data.GameData.GameMap
{
    public static class Map
    {
        private static Dictionary<int, List<MapObject>> mapContents = new Dictionary<int, List<MapObject>>();

        #region Basic map create and delete
        /// <summary>
        /// Creates an empty map in the map store.
        /// Creating max int maps would cause an overflow somewhere, but should never happen!
        /// </summary>
        /// <returns>The identifier for the new map.</returns>        
        public static int CreateMap()
        {
            // Determine the current maximum key being used in the map collection and increment by one.
            Dictionary<int, List<MapObject>>.KeyCollection keyColl = mapContents.Keys;
            int newMapId = 1;
            if( keyColl.Count > 0)
                newMapId = keyColl.Max() + 1;

            // Check the id is not used (should always pass) and create a new map
            if (!mapContents.ContainsKey(newMapId))
                mapContents.Add(newMapId, new List<MapObject>());

            return newMapId;
        }

        /// <summary>
        /// Deletes a map from the store.
        /// </summary>
        /// <param name="obj">The identifier of the map to be deleted.</param>
        public static void DeleteMap(int mapId)
        {
            if (mapContents.ContainsKey(mapId))
                mapContents.Remove(mapId);
        }
        #endregion

        #region Basic map content add, delete
        /// <summary>
        /// Add a new map object to the mapContent store.
        /// </summary>
        /// <param name="obj">The object to place on the map</param>
        public static void Add(MapObject obj)
        {
            List<MapObject> mapObjects = null;
            int mapId = obj.MapPosition.MapId;

            // If this is a new map then create a new object store.
            if (!mapContents.ContainsKey(mapId))
                mapContents.Add(obj.MapPosition.MapId, new List<MapObject>());

            mapObjects = mapContents[mapId];

            if (!mapObjects.Contains(obj))
                mapObjects.Add(obj);
        }

        // NOT NEED (I THINK) AS THE POSITION IS HELD IN THE OBJCET, NOT IN THE MAP.
        // ONLY HAVE TO UPDATE THE MAPID IF IT CHANGES
        //
        //internal static void Move(GameObject obj, MapIndex newIndex)
        //{
        //    List<GameObject> mapObjects = mapContents[obj.MapPosition.MapId];

        //    if (mapObjects.Contains(obj))
        //    {
        //        mapContents.Remove(obj.MapPosition);
        //        mapContents.Add(newIndex, obj);
        //    }
        //}

        /// <summary>
        /// Remove an object from the mapContents store
        /// </summary>
        /// <param name="obj">The object to remove from the map</param>
        public static void Delete(MapObject obj)
        {
            List<MapObject> mapObjects = mapContents[obj.MapPosition.MapId];
            mapObjects.Remove(obj);
        }
        #endregion

        #region Map helper methods
        /// <summary>
        /// Return the distance between the two map positions
        /// </summary>
        /// <param name="start">The start position</param>
        /// <param name="end">The end position</param>
        /// <returns>The distance (in hexes) between the two positions</returns>
        /*
        public static int DistanceTo(MapIndex start, MapIndex end)
        {
            int deltaX = Math.Abs(start.Column - end.Column);
            int deltaY = Math.Abs(start.Row - end.Row);

            // =IF(Sheet1!$A$4=EVEN(Sheet1!$A$4),Sheet1!$B$4-ROUNDDOWN(DeltaX/2,0),Sheet1!$B$4-ROUNDUP(DeltaX/2,0))
            // =IF(Sheet1!$A$4=EVEN(Sheet1!$A$4),Sheet1!$B$4+ROUNDUP(DeltaX/2,0),Sheet1!$B$4+ROUNDDOWN(DeltaX/2,0))
            int ya;
            int yb;
            if (start.Column == Even(start.Column))
            {
                ya = start.Row - (int)Math.Floor((double)(deltaX / 2));
                yb = start.Row + (int)Math.Ceiling((double)(deltaX / 2));
            }
            else
            {
                ya = start.Row - (int)Math.Ceiling((double)(deltaX / 2));
                yb = start.Row + (int)Math.Floor((double)(deltaX / 2));
            }

            // =IF(AND(Ya<=D4,D4<=Yb), DeltaX, DeltaX+MIN(ABS(D4-Ya),ABS(D4-Yb)))
            int result;

            if (ya <= end.Row && end.Row <= yb)
                result = deltaX;
            else
                result = deltaX + Math.Min(Math.Abs(end.Row - ya), Math.Abs(end.Row - yb));

            return result;
        }

        static private int Even(int value)
        {
            int v = Math.Abs(value);
            int r;
            Math.DivRem(v, 2, out r);
            if (r != 0)
                v += 1;

            if (value < 0)
                v *= -1;

            return v;
        }
        */
        public static int DistanceTo(MapIndex start, MapIndex end)
        {
            bool evenCol = (start.Column % 2) == 0;

            if (!evenCol)
                return Math.Max(Math.Abs(start.Row - end.Row), Math.Abs(start.Column - end.Column));
            else
            {
                MapIndex tStart = MapPositionFrom(start, 3, 1, false);
                MapIndex tEnd = MapPositionFrom(end, 3, 1, false);

                return Math.Max(Math.Abs(tStart.Row - tEnd.Row), Math.Abs(tStart.Column - tEnd.Column));
            }
        }        

        /// <summary>
        /// Returns the angle (heading) between the two map positions
        /// </summary>
        /// <param name="start">The start poition</param>
        /// <param name="end">The end position</param>
        /// <returns>The angle (0,1,3,5,7,9,11) from start to end</returns>
        public static int AngleTo(MapIndex start, MapIndex end)
        {
            if (start.Equals(end))
                return 0;

            // Get the distance between the two
            int distance = DistanceTo(start, end);
            bool evenCol = (start.Column % 2) == 0;

            if (Math.Abs(end.Column - start.Column) == distance)
            {
                if (end.Column > start.Column)
                {
                    if (evenCol)
                    {
                        if (end.Row <= start.Row)
                            return 3;
                        else
                            return 5;
                    }
                    else
                    {
                        if (end.Row < start.Row)
                            return 3;
                        else
                            return 5;
                    }
                }
                else
                {
                    if (evenCol)
                    {
                        if (end.Row <= start.Row)
                            return 11;
                        else
                            return 9;
                    }
                    else
                    {
                        if (end.Row < start.Row)
                            return 11;
                        else
                            return 9;
                    }
                }
            }
            else
            {
                if (end.Row < start.Row)
                    return 1;
                else
                    return 7;
            }
        }

        /// <summary>
        /// Calculate a new map position based and the heading and distance from a known position.  Can be in the
        /// hexes or in LM (12 per hex).
        /// </summary>
        /// <param name="startPos">Where to calculate from.</param>
        /// <param name="direction">1 to 12: 1 is North, 7 South, etc.</param>
        /// <param name="distance">Distance to calculate.</param>
        /// <param name="inLM">If the distance is in Light Minutes (12 per hex on the strategic map).</param>
        /// <returns>The calculated map position (always on the same map).</returns>
        public static MapIndex MapPositionFrom(MapIndex startPos, int direction, int distance, bool inLM)
        {
            MapIndex finalPosition = null;
            
            // System Hexes are in multiples of 12LMs
            int dist = distance;
            if (inLM)
            {
                dist = (distance / 12);
                if (distance % 12 != 0)
                    ++dist;
            }

            bool evenCol = (startPos.Column % 2) == 0;

            int dRow = 0;
            int dCol = 0;
            double halfDist = (double)dist / 2;

            switch (direction)
            {
                case 1: // North
                    {
                        dRow -= dist;
                        dCol = 0;

                        finalPosition = new MapIndex(startPos.MapId, startPos.Row + dRow, startPos.Column + dCol);
                    }
                    break;
                case 2:
                    {
                        if (evenCol) halfDist += 0.1;
                        MapIndex temp = MapPositionFrom(startPos, 1, dist, false);

                        finalPosition = MapPositionFrom(temp, 5, (int)halfDist, false);
                    }
                    break;
                case 3: // North-East
                    {
                        // Even = (dist/2) round up
                        // Odd = (dist/2) round down
                        if (evenCol) halfDist += 0.1;
                        dRow -= (int)Math.Round(halfDist);
                        dCol += dist;
                    
                        finalPosition = new MapIndex(startPos.MapId, startPos.Row + dRow, startPos.Column + dCol);
                    }
                    break;
                case 4:
                    {
                        if (evenCol) halfDist += 0.1;
                        MapIndex temp = MapPositionFrom(startPos, 3, dist, false);

                        finalPosition = MapPositionFrom(temp, 7, (int)halfDist, false);
                    }
                    break;
                case 5: // South-East
                    {
                        // Even = (dist/2) round down
                        // Odd = (dist/2) round up
                        if (!evenCol) halfDist += 0.1;
                        dRow += (int)Math.Round(halfDist);
                        dCol += dist;

                        finalPosition = new MapIndex(startPos.MapId, startPos.Row + dRow, startPos.Column + dCol);
                    }
                    break;
                case 6:
                    {
                        if (evenCol) halfDist += 0.1;
                        MapIndex temp = MapPositionFrom(startPos, 5, dist, false);

                        finalPosition = MapPositionFrom(temp, 9, (int)halfDist, false);
                    }
                    break;
                case 7: // South
                    {
                        dRow += dist;
                        dCol = 0;

                        finalPosition = new MapIndex(startPos.MapId, startPos.Row + dRow, startPos.Column + dCol);
                    }
                    break;
                case 8:
                    {
                        if (evenCol) halfDist += 0.1;
                        MapIndex temp = MapPositionFrom(startPos, 7, dist, false);

                        finalPosition = MapPositionFrom(temp, 11, (int)halfDist, false);
                    }
                    break;
                case 9: // South-West
                    {
                        // Even = (dist/2) round down
                        // Odd = (dist/2) round up
                        if (!evenCol) halfDist += 0.1;
                        dRow += (int)Math.Round(halfDist);
                        dCol -= dist;

                        finalPosition = new MapIndex(startPos.MapId, startPos.Row + dRow, startPos.Column + dCol);
                    }
                    break;
                case 10:
                    {
                        if (evenCol) halfDist += 0.1;
                        MapIndex temp = MapPositionFrom(startPos, 9, dist, false);

                        finalPosition = MapPositionFrom(temp, 1, (int)halfDist, false);
                    }
                    break;
                case 11: // North-West
                    {
                        // Even = (dist/2) round up
                        // Odd = (dist/2) round down
                        if (evenCol) halfDist += 0.1;
                        dRow -= (int)Math.Round(halfDist);
                        dCol -= dist;

                        finalPosition = new MapIndex(startPos.MapId, startPos.Row + dRow, startPos.Column + dCol);
                    }
                    break;
                case 12:
                    {
                        if (evenCol) halfDist += 0.1;
                        MapIndex temp = MapPositionFrom(startPos, 11, dist, false);

                        finalPosition = MapPositionFrom(temp, 3, (int)halfDist, false);
                    }
                    break;
                default:
                    throw new NotSupportedException("Invalid Direction");
            }           

            return finalPosition;
        }
        #endregion

        #region Map content retrival
        /// <summary>
        /// Retrieve every object on a specific map
        /// </summary>
        /// <param name="mapId">The map data to retrieve</param>
        /// <returns>The list of game objects on the map</returns>
        public static List<MapObject> GetMapContents(int mapId)
        {
            if (mapContents.ContainsKey(mapId))
                return mapContents[mapId];
            else
                return new List<MapObject>();
        }

        /// <summary>
        /// Retrieve only the objects matching the specified player on the indicated map
        /// </summary>
        /// <param name="mapId">The map to be checked</param>
        /// <param name="ownerId">The owner identifier to find</param>
        /// <returns>The list of game objects on the map belonging to the player</returns>
        public static List<MapObject> GetMapContentsByOwner(int mapId, Guid ownerId)
        {
            List<MapObject> mapObjects = mapContents[mapId];
            List<MapObject> objList = new List<MapObject>();

            foreach (MapObject obj in mapObjects)
            {
                if (obj.OwnerId == ownerId)
                    objList.Add(obj);
            }

            return objList;
        }

        /// <summary>
        /// Retrieve the list of objects within the specified distance of the map position.  This only
        /// returns game objects NOT belonging to the player specified and is not neutral (Guid.Empty).
        /// </summary>
        /// <param name="mapPosition">The centre of the area to check</param>
        /// <param name="radius">The radius of the circle to check</param>
        /// <param name="ownerId">The identifier of the owner to IGNORE</param>
        /// <returns>The list of game objects on the map NOT belonging to the player or neutral</returns>
        public static List<MapObject> GetMapContentsAround(MapIndex mapPosition, int radius, Guid ownerId)
        {
            List<MapObject> mapObjects = mapContents[mapPosition.MapId];
            List<MapObject> objList = new List<MapObject>();

            foreach (MapObject obj in mapObjects)
            {
                if (obj.OwnerId != ownerId &&
                    obj.OwnerId != Guid.Empty &&
                    DistanceTo(mapPosition, obj.MapPosition) <= radius)
                {
                    objList.Add(obj);
                }
            }

            return objList;
        }

        /// <summary>
        /// Retrieve the contents of a specific map hex
        /// </summary>
        /// <param name="mapPosition">The position to check</param>
        /// <returns>The list of objects at the specified map position</returns>
        public static List<MapObject> GetMapHexContents(MapIndex mapPosition)
        {
            List<MapObject> mapObjects = mapContents[mapPosition.MapId];
            List<MapObject> objList = new List<MapObject>();

            foreach (MapObject obj in mapObjects)
            {
                if (obj.MapPosition.Equals(mapPosition))
                {
                    objList.Add(obj);
                }
            }

            return objList;
        }
        #endregion
    }
}
